﻿using System;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
using System.Text;
using WirelessAccessPoint.Core;
using WirelessAccessPoint.InteropServices;
using WirelessAccessPoint.Model;
using WirelessAccessPoint.Resources;

namespace WirelessAccessPoint.Services
{
    /// <summary>
    /// Hosted Networkを処理するサービスです。
    /// </summary>
    public class HostedNetworkService
    {
        #region Service

        /// <summary>
        /// Hosted Networkの状態を取得します。
        /// </summary>
        /// <param name="selectedHostedNetwork">
        /// 選択されているHosted Network。
        /// </param>
        /// <returns>
        /// Hosted Networkの状態。
        /// </returns>
        public HostedNetworkContext Update(HostedNetwork selectedHostedNetwork)
        {
            var context = new HostedNetworkContext();
            context.SelectedHostedNetwork = selectedHostedNetwork;
            IntPtr handle = IntPtr.Zero;

            try
            {
                this.OpenHandle(out handle);
                this.InitSettings(handle);
                this.QueryStatus(handle, context);
                this.QueryProperty(handle, context);
                this.QuerySecondaryKey(handle, context);
                this.GetIpNetTable(context);
            }
            finally
            {
                this.CloseHandle(handle);
            }

            return context;
        }

        /// <summary>
        /// Hosted Networkを開始します。
        /// </summary>
        /// <param name="selectedHostedNetwork">
        /// 選択されているHosted Network。
        /// </param>
        public void Enable(HostedNetwork selectedHostedNetwork)
        {
            IntPtr handle = IntPtr.Zero;

            try
            {
                this.OpenHandle(out handle);
                this.SetProperty(handle, selectedHostedNetwork);
                this.SetSecondaryKey(handle, selectedHostedNetwork);
                this.ForceStop(handle);
                this.ForceStart(handle);
            }
            finally
            {
                this.CloseHandle(handle);
            }
        }

        /// <summary>
        /// Hosted Networkを停止します。
        /// </summary>
        public void Disable()
        {
            IntPtr handle = IntPtr.Zero;

            try
            {
                this.OpenHandle(out handle);
                this.ForceStop(handle);
            }
            finally
            {
                this.CloseHandle(handle);
            }
        }

        #endregion

        #region Wlanapi

        private void OpenHandle(out IntPtr handle)
        {
            handle = IntPtr.Zero;
            uint negotiatedVersion;

            var error = Wlanapi.WlanOpenHandle(
                2,
                IntPtr.Zero,
                out negotiatedVersion,
                ref handle);

            if (error != WinError.ERROR_SUCCESS)
            {
                throw new Exception(this.FormatMessage("WlanOpenHandle", error));
            }
        }

        private void CloseHandle(IntPtr handle)
        {
            if (handle != IntPtr.Zero)
            {
                var error = Wlanapi.WlanCloseHandle(
                    handle,
                    IntPtr.Zero);

                if (error != WinError.ERROR_SUCCESS)
                {
                    throw new Exception(this.FormatMessage("WlanCloseHandle", error));
                }
            }
        }

        private void InitSettings(IntPtr handle)
        {
            WLAN_HOSTED_NETWORK_REASON failReason;

            var error = Wlanapi.WlanHostedNetworkInitSettings(
                handle,
                out failReason,
                IntPtr.Zero);

            if (error != WinError.ERROR_SUCCESS)
            {
                throw new Exception(this.FormatMessage("WlanHostedNetworkInitSettings", error, failReason));
            }

            if (failReason != WLAN_HOSTED_NETWORK_REASON.wlan_hosted_network_reason_success)
            {
                throw new Exception(this.FormatMessage("WlanHostedNetworkInitSettings", failReason));
            }
        }

        private void QueryStatus(IntPtr handle, HostedNetworkContext context)
        {
            IntPtr ppWlanHostedNetworkStatus = IntPtr.Zero;

            try
            {
                var error = Wlanapi.WlanHostedNetworkQueryStatus(
                    handle,
                    out ppWlanHostedNetworkStatus,
                    IntPtr.Zero);

                if (error != WinError.ERROR_SUCCESS)
                {
                    throw new Exception(this.FormatMessage("WlanHostedNetworkQueryStatus", error));
                }

                var status = ppWlanHostedNetworkStatus.ToStructure<WLAN_HOSTED_NETWORK_STATUS>();
                context.ActiveHostedNetwork.HostedNetworkState = status.HostedNetworkState;

                if (status.HostedNetworkState != WLAN_HOSTED_NETWORK_STATE.wlan_hosted_network_unavailable)
                {
                    context.ActiveHostedNetwork.DeviceID = status.IPDeviceID;
                    context.ActiveHostedNetwork.BSSID = status.wlanHostedNetworkBSSID.ToStringMacAddress();
                }

                if (status.HostedNetworkState == WLAN_HOSTED_NETWORK_STATE.wlan_hosted_network_active)
                {
                    context.ActiveHostedNetwork.ChannelFrequency = status.ulChannelFrequency;
                    context.ActiveHostedNetwork.PhyType = status.dot11PhyType;

                    var statusOffset = Marshal.OffsetOf(typeof(WLAN_HOSTED_NETWORK_STATUS), "PeerList");
                    var statePtr = new IntPtr(ppWlanHostedNetworkStatus.ToInt64() + statusOffset.ToInt64());              
                    var stateSize = Marshal.SizeOf(typeof(WLAN_HOSTED_NETWORK_PEER_STATE));

                    for (int i = 0; i < status.dwNumberOfPeers; i++)
                    {
                        var state = statePtr.ToStructure<WLAN_HOSTED_NETWORK_PEER_STATE>();
                        statePtr = IntPtr.Add(statePtr, stateSize);

                        context.HostedNetworkPeers.Add(new HostedNetworkPeer()
                        {
                            PeerMacAddress = state.PeerMacAddress.ToStringMacAddress(),
                            PeerAuthState = state.PeerAuthState
                        });
                    }
                }
            }
            finally
            {
                if (ppWlanHostedNetworkStatus != IntPtr.Zero)
                {
                    Wlanapi.WlanFreeMemory(ppWlanHostedNetworkStatus);
                    ppWlanHostedNetworkStatus = IntPtr.Zero;
                }
            }
        }

        private void QueryProperty(IntPtr handle, HostedNetworkContext context)
        {
            IntPtr connectionSettingsData = IntPtr.Zero;
            IntPtr securitySettingsData = IntPtr.Zero;

            try
            {
                uint dataSize;
                WLAN_OPCODE_VALUE_TYPE wlanOpcodeValueType;

                var error = Wlanapi.WlanHostedNetworkQueryProperty(
                    handle,
                    WLAN_HOSTED_NETWORK_OPCODE.wlan_hosted_network_opcode_connection_settings,
                    out dataSize,
                    out connectionSettingsData,
                    out wlanOpcodeValueType,
                    IntPtr.Zero);

                if (error != WinError.ERROR_SUCCESS)
                {
                    throw new Exception(this.FormatMessage("WlanHostedNetworkQueryProperty", error));
                }

                var settings = connectionSettingsData.ToStructure<WLAN_HOSTED_NETWORK_CONNECTION_SETTINGS>();
                context.ActiveHostedNetwork.SSID = settings.hostedNetworkSSID.ucSSID;
                context.ActiveHostedNetwork.MaxNumberOfPeers = settings.dwMaxNumberOfPeers;

                if (!string.IsNullOrEmpty(settings.hostedNetworkSSID.ucSSID))
                {
                    context.SelectedHostedNetwork.SSID = settings.hostedNetworkSSID.ucSSID;
                }
            }
            finally
            {
                if (connectionSettingsData != IntPtr.Zero)
                {
                    Wlanapi.WlanFreeMemory(connectionSettingsData);
                    connectionSettingsData = IntPtr.Zero;
                }
            }

            try
            {
                uint dataSize;
                WLAN_OPCODE_VALUE_TYPE wlanOpcodeValueType;

                var error = Wlanapi.WlanHostedNetworkQueryProperty(
                    handle,
                    WLAN_HOSTED_NETWORK_OPCODE.wlan_hosted_network_opcode_security_settings,
                    out dataSize,
                    out securitySettingsData,
                    out wlanOpcodeValueType,
                    IntPtr.Zero);

                if (error != WinError.ERROR_SUCCESS)
                {
                    throw new Exception(this.FormatMessage("WlanHostedNetworkQueryProperty", error));
                }

                var settings = securitySettingsData.ToStructure<WLAN_HOSTED_NETWORK_SECURITY_SETTINGS>();
                context.ActiveHostedNetwork.AuthAlgorithm = settings.dot11AuthAlgo;
                context.ActiveHostedNetwork.CipherAlgorithm = settings.dot11CipherAlgo;
            }
            finally
            {
                if (securitySettingsData != IntPtr.Zero)
                {
                    Wlanapi.WlanFreeMemory(securitySettingsData);
                    securitySettingsData = IntPtr.Zero;
                }
            }
        }

        private void QuerySecondaryKey(IntPtr handle, HostedNetworkContext context)
        {
            uint keyLength;
            IntPtr keyData = IntPtr.Zero;
            bool isPassPhrase;
            bool persistent;
            WLAN_HOSTED_NETWORK_REASON failReason;

            try
            {
                var error = Wlanapi.WlanHostedNetworkQuerySecondaryKey(
                    handle,
                    out keyLength,
                    out keyData,
                    out isPassPhrase,
                    out persistent,
                    out failReason,
                    IntPtr.Zero);

                if (error != WinError.ERROR_SUCCESS)
                {
                    throw new Exception(this.FormatMessage("WlanHostedNetworkQuerySecondaryKey", error, failReason));
                }

                if (failReason != WLAN_HOSTED_NETWORK_REASON.wlan_hosted_network_reason_success)
                {
                    throw new Exception(this.FormatMessage("WlanHostedNetworkQuerySecondaryKey", failReason));
                }

                var passPhrase = Marshal.PtrToStringAnsi(keyData);
                context.ActiveHostedNetwork.PassPhrase = passPhrase;

                if (!string.IsNullOrEmpty(passPhrase))
                {
                    context.SelectedHostedNetwork.PassPhrase = passPhrase;
                }
            }
            finally
            {
                if (keyData != IntPtr.Zero)
                {
                    Wlanapi.WlanFreeMemory(keyData);
                    keyData = IntPtr.Zero;
                }
            }
        }

        private void SetProperty(IntPtr handle, HostedNetwork selectedHostedNetwork)
        {
            var enablePtr = IntPtr.Zero;
            var connectionSettingsPtr = IntPtr.Zero;

            try
            {
                enablePtr = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(bool)));
                Marshal.StructureToPtr(true, enablePtr, false);
                WLAN_HOSTED_NETWORK_REASON failReason;

                var error = Wlanapi.WlanHostedNetworkSetProperty(
                    handle,
                    WLAN_HOSTED_NETWORK_OPCODE.wlan_hosted_network_opcode_enable,
                    (uint) Marshal.SizeOf(typeof(bool)),
                    enablePtr,
                    out failReason,
                    IntPtr.Zero);

                if (error != WinError.ERROR_SUCCESS)
                {
                    throw new Exception(this.FormatMessage("WlanHostedNetworkSetProperty", error, failReason));
                }

                if (failReason != WLAN_HOSTED_NETWORK_REASON.wlan_hosted_network_reason_success)
                {
                    throw new Exception(this.FormatMessage("WlanHostedNetworkSetProperty", failReason));
                }
            }
            finally
            {
                if (enablePtr != IntPtr.Zero)
                {
                    Marshal.FreeCoTaskMem(enablePtr);
                    enablePtr = IntPtr.Zero;
                }
            }

            try
            {
                var settings = new WLAN_HOSTED_NETWORK_CONNECTION_SETTINGS()
                {
                    dwMaxNumberOfPeers = 100,
                    hostedNetworkSSID = new DOT11_SSID()
                    {
                        ucSSID = selectedHostedNetwork.SSID,
                        uSSIDLength = (uint) selectedHostedNetwork.SSID.Length
                    }
                };

                connectionSettingsPtr = Marshal.AllocCoTaskMem(Marshal.SizeOf(settings));
                Marshal.StructureToPtr(settings, connectionSettingsPtr, false);
                WLAN_HOSTED_NETWORK_REASON failReason;

                var error = Wlanapi.WlanHostedNetworkSetProperty(
                    handle,
                    WLAN_HOSTED_NETWORK_OPCODE.wlan_hosted_network_opcode_connection_settings,
                    (uint) Marshal.SizeOf(settings),
                    connectionSettingsPtr,
                    out failReason,
                    IntPtr.Zero);

                if (error != WinError.ERROR_SUCCESS)
                {
                    throw new Exception(this.FormatMessage("WlanHostedNetworkSetProperty", error, failReason));
                }

                if (failReason != WLAN_HOSTED_NETWORK_REASON.wlan_hosted_network_reason_success)
                {
                    throw new Exception(this.FormatMessage("WlanHostedNetworkSetProperty", failReason));
                }
            }
            finally
            {
                if (connectionSettingsPtr != IntPtr.Zero)
                {
                    Marshal.FreeCoTaskMem(connectionSettingsPtr);
                    connectionSettingsPtr = IntPtr.Zero;
                }
            }
        }

        private void SetSecondaryKey(IntPtr handle, HostedNetwork selectedHostedNetwork)
        {
            WLAN_HOSTED_NETWORK_REASON failReason;

            var error = Wlanapi.WlanHostedNetworkSetSecondaryKey(
                handle,
                (uint) (selectedHostedNetwork.PassPhrase.Length + 1),
                selectedHostedNetwork.PassPhrase,
                true,
                true,
                out failReason,
                IntPtr.Zero);

            if (error != WinError.ERROR_SUCCESS)
            {
                throw new Exception(this.FormatMessage("WlanHostedNetworkSetSecondaryKey", error, failReason));
            }

            if (failReason != WLAN_HOSTED_NETWORK_REASON.wlan_hosted_network_reason_success)
            {
                throw new Exception(this.FormatMessage("WlanHostedNetworkSetSecondaryKey", failReason));
            }
        }

        private void ForceStart(IntPtr handle)
        {
            WLAN_HOSTED_NETWORK_REASON failReason;

            var error = Wlanapi.WlanHostedNetworkForceStart(
                handle,
                out failReason,
                IntPtr.Zero);

            if (error != WinError.ERROR_SUCCESS)
            {
                throw new Exception(this.FormatMessage("WlanHostedNetworkForceStart", error, failReason));
            }

            if (failReason != WLAN_HOSTED_NETWORK_REASON.wlan_hosted_network_reason_success)
            {
                throw new Exception(this.FormatMessage("WlanHostedNetworkForceStart", failReason));
            }
        }

        private void ForceStop(IntPtr handle)
        {
            WLAN_HOSTED_NETWORK_REASON failReason;

            var error = Wlanapi.WlanHostedNetworkForceStop(
                handle,
                out failReason,
                IntPtr.Zero);

            if (error != WinError.ERROR_SUCCESS)
            {
                throw new Exception(this.FormatMessage("WlanHostedNetworkForceStop", error, failReason));
            }

            if (failReason != WLAN_HOSTED_NETWORK_REASON.wlan_hosted_network_reason_success)
            {
                throw new Exception(this.FormatMessage("WlanHostedNetworkForceStop", failReason));
            }
        }

        #endregion

        #region Iphlpapi

        private void GetIpNetTable(HostedNetworkContext context)
        {
            if (context.HostedNetworkPeers.Count != 0)
            {
                IntPtr pIpNetTable = IntPtr.Zero;

                try
                {
                    uint pdwSize = 0;
                    var error = Iphlpapi.GetIpNetTable(IntPtr.Zero, ref pdwSize, true);

                    if (error != WinError.ERROR_SUCCESS && error != WinError.ERROR_INSUFFICIENT_BUFFER)
                    {
                        throw new Exception(this.FormatMessage("GetIpNetTable", error));
                    }

                    pIpNetTable = Marshal.AllocCoTaskMem((int) pdwSize);
                    error = Iphlpapi.GetIpNetTable(pIpNetTable, ref pdwSize, true);

                    if (error != WinError.ERROR_SUCCESS)
                    {
                        throw new Exception(this.FormatMessage("GetIpNetTable", error));
                    }

                    var table = pIpNetTable.ToStructure<MIB_IPNETTABLE>();
                    var tableOffset = Marshal.OffsetOf(typeof(MIB_IPNETTABLE), "table");
                    var rowPtr = new IntPtr(pIpNetTable.ToInt64() + tableOffset.ToInt64());
                    var rowSize = Marshal.SizeOf(typeof(MIB_IPNETROW));

                    for (int i = 0; i < table.dwNumEntries; i++)
                    {
                        var row = rowPtr.ToStructure<MIB_IPNETROW>();
                        rowPtr = IntPtr.Add(rowPtr, rowSize);

                        if (row.dwPhysAddrLen != 0 && row.dwType != MIB_IPNET_TYPE.MIB_IPNET_TYPE_INVALID)
                        {
                            var macAddress = row.bPhysAddr.ToStringMacAddress((int) row.dwPhysAddrLen);
                            var peer = context.HostedNetworkPeers.FirstOrDefault(p => p.PeerMacAddress == macAddress);

                            if (peer != null)
                            {
                                var ipAddress = new IPAddress(row.dwAddr);
                                peer.PeerIPAddress = ipAddress;
                            }
                        }
                    }
                }
                finally
                {
                    if (pIpNetTable != IntPtr.Zero)
                    {
                        Marshal.FreeCoTaskMem(pIpNetTable);
                        pIpNetTable = IntPtr.Zero;
                    }
                }
            }
        }

        #endregion

        #region Kernel32

        private string FormatMessage(string function, uint error)
        {
            return string.Format(
                "{0}\nFunction: {1}\nMessage: {2}",
                MessageResource.CommandNotExecuteMessage,
                function,
                this.FormatMessage(error));
        }

        private string FormatMessage(string function, WLAN_HOSTED_NETWORK_REASON failReason)
        {
            return string.Format(
                "{0}\nFunction: {1}\nReason: {2}",
                MessageResource.CommandNotExecuteMessage,
                function,
                failReason);
        }

        private string FormatMessage(string function, uint error, WLAN_HOSTED_NETWORK_REASON failReason)
        {
            return string.Format(
                "{0}\nFunction: {1}\nMessage: {2}\nReason: {3}",
                MessageResource.CommandNotExecuteMessage,
                function,
                this.FormatMessage(error),
                failReason);
        }

        private string FormatMessage(uint error)
        {
            var lpBuffer = new StringBuilder(256);
            Kernel32.FormatMessage(WinBase.FORMAT_MESSAGE_FROM_SYSTEM, IntPtr.Zero, error, 0, lpBuffer, lpBuffer.Capacity, IntPtr.Zero);
            var message = lpBuffer.ToString();
            message = message.Replace("\r", string.Empty);
            message = message.Replace("\n", string.Empty);
            return message;
        }

        #endregion
    }
}
